summaryrefslogtreecommitdiff
path: root/app/[lng]/evcp/(evcp)/menu-access-dept
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept')
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx380
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx309
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx343
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/_components/domain-constants.ts52
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/page.tsx34
5 files changed, 0 insertions, 1118 deletions
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx
deleted file mode 100644
index f8a75641..00000000
--- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx
+++ /dev/null
@@ -1,380 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { Loader2, Users, Building2, AlertCircle } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Textarea } from "@/components/ui/textarea";
-import { Badge } from "@/components/ui/badge";
-import { Label } from "@/components/ui/label";
-import { Separator } from "@/components/ui/separator";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- DepartmentNode
-} from "@/lib/users/knox-service";
-import {
- getDepartmentDomainAssignmentsByDepartments
-} from "@/lib/users/department-domain/service";
-import { DOMAIN_OPTIONS, getDomainLabel } from "./domain-constants";
-
-interface ExistingAssignment {
- id: number;
- companyCode: string;
- departmentCode: string;
- departmentName: string;
- assignedDomain: string;
- description?: string | null;
- createdAt: Date;
- updatedAt: Date;
-}
-
-interface DepartmentDomainAssignmentDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- selectedDepartments: string[];
- departments: DepartmentNode[];
- companyInfo: { code: string; name: string };
- onAssign: (assignments: {
- departmentCodes: string[];
- domain: string;
- description?: string;
- }) => Promise<void>;
- isLoading?: boolean;
-}
-
-export function DepartmentDomainAssignmentDialog({
- open,
- onOpenChange,
- selectedDepartments,
- departments,
- companyInfo,
- onAssign,
- isLoading = false,
-}: DepartmentDomainAssignmentDialogProps) {
- const [selectedDomain, setSelectedDomain] = React.useState<string>("");
- const [description, setDescription] = React.useState<string>("");
- const [isSubmitting, setIsSubmitting] = React.useState(false);
- const [existingAssignments, setExistingAssignments] = React.useState<ExistingAssignment[]>([]);
- const [isLoadingAssignments, setIsLoadingAssignments] = React.useState(false);
-
- // 선택된 부서들의 정보 가져오기
- const getSelectedDepartmentInfo = React.useCallback(() => {
- const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => {
- for (const node of nodes) {
- if (node.departmentCode === code) {
- return node;
- }
- const found = findDepartment(node.children, code);
- if (found) return found;
- }
- return null;
- };
-
- return selectedDepartments
- .map(code => findDepartment(departments, code))
- .filter(Boolean) as DepartmentNode[];
- }, [departments, selectedDepartments]);
-
- // 회사별로 그룹화
- const selectedDepartmentsByCompany = React.useMemo(() => {
- const deptInfo = getSelectedDepartmentInfo();
- const grouped = new Map<string, DepartmentNode[]>();
-
- deptInfo.forEach(dept => {
- if (!grouped.has(dept.companyCode)) {
- grouped.set(dept.companyCode, []);
- }
- grouped.get(dept.companyCode)!.push(dept);
- });
-
- return grouped;
- }, [getSelectedDepartmentInfo]);
-
- // 기존 할당 정보 조회
- React.useEffect(() => {
- if (open && selectedDepartments.length > 0) {
- const loadExistingAssignments = async () => {
- setIsLoadingAssignments(true);
- try {
- const assignments = await getDepartmentDomainAssignmentsByDepartments(selectedDepartments);
- setExistingAssignments(assignments as ExistingAssignment[]);
- } catch (error) {
- console.error("기존 할당 정보 조회 실패:", error);
- setExistingAssignments([]);
- } finally {
- setIsLoadingAssignments(false);
- }
- };
-
- loadExistingAssignments();
- } else {
- setExistingAssignments([]);
- }
- }, [open, selectedDepartments]);
-
- // 폼 초기화
- React.useEffect(() => {
- if (open) {
- setSelectedDomain("");
- setDescription("");
- setIsSubmitting(false);
- }
- }, [open]);
-
- // 할당 처리
- const handleAssign = async () => {
- if (!selectedDomain || selectedDepartments.length === 0) {
- return;
- }
-
- setIsSubmitting(true);
-
- try {
- await onAssign({
- departmentCodes: selectedDepartments,
- domain: selectedDomain,
- description: description.trim() || undefined,
- });
-
- // 성공 시 다이얼로그 닫기
- onOpenChange(false);
- } catch (error) {
- console.error("도메인 할당 실패:", error);
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const canSubmit = selectedDomain && selectedDepartments.length > 0 && !isSubmitting && !isLoading;
- const selectedDomainInfo = DOMAIN_OPTIONS.find(opt => opt.value === selectedDomain);
- const hasConflicts = existingAssignments.some(a => a.assignedDomain !== selectedDomain && selectedDomain);
-
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <Building2 className="h-5 w-5" />
- 부서별 도메인 할당
- </DialogTitle>
- <DialogDescription>
- 선택된 {selectedDepartments.length}개 부서에 도메인을 할당합니다.
- 상위 부서를 선택한 경우 하위 부서들도 자동으로 포함됩니다.
- </DialogDescription>
- </DialogHeader>
-
- <div className="flex-1 overflow-y-auto px-1">
- <div className="space-y-6 pr-3">
- {/* 선택된 부서들 표시 */}
- <div className="space-y-3">
- <Label className="text-sm font-medium flex items-center gap-2">
- <Users className="h-4 w-4" />
- 선택된 부서 ({selectedDepartments.length}개)
- </Label>
-
- <div className="border rounded-md p-3 max-h-40 overflow-y-auto">
- {Array.from(selectedDepartmentsByCompany.entries()).map(([companyCode, depts]) => (
- <div key={companyCode} className="mb-3 last:mb-0">
- <div className="text-sm font-medium text-muted-foreground mb-2">
- {companyCode} - {companyInfo.name}
- </div>
- <div className="flex flex-wrap gap-2">
- {depts.map((dept) => (
- <Badge
- key={dept.departmentCode}
- variant="outline"
- className="text-xs"
- >
- {dept.departmentName || dept.departmentCode}
- </Badge>
- ))}
- </div>
- </div>
- ))}
- </div>
- </div>
-
- {/* 기존 할당 현황 */}
- {(existingAssignments.length > 0 || isLoadingAssignments) && (
- <>
- <Separator />
- <div className="space-y-3">
- <Label className="text-sm font-medium flex items-center gap-2">
- <AlertCircle className="h-4 w-4" />
- 현재 할당 현황
- </Label>
-
- {isLoadingAssignments ? (
- <div className="flex items-center justify-center py-4">
- <Loader2 className="h-4 w-4 animate-spin mr-2" />
- 기존 할당 정보를 조회하는 중...
- </div>
- ) : (
- <div className="border rounded-md max-h-60 overflow-y-auto">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>부서</TableHead>
- <TableHead>현재 도메인</TableHead>
- <TableHead>할당일</TableHead>
- <TableHead>설명</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {existingAssignments.map((assignment) => (
- <TableRow key={assignment.id}>
- <TableCell className="font-medium">
- {assignment.departmentName}
- </TableCell>
- <TableCell>
- <Badge
- variant={assignment.assignedDomain === 'evcp' ? 'default' : 'secondary'}
- >
- {getDomainLabel(assignment.assignedDomain)}
- </Badge>
- </TableCell>
- <TableCell className="text-sm text-muted-foreground">
- {new Date(assignment.createdAt).toLocaleDateString('ko-KR')}
- </TableCell>
- <TableCell className="max-w-xs truncate text-sm">
- {assignment.description || '-'}
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- )}
-
- {hasConflicts && (
- <div className="bg-yellow-50 border-yellow-200 border rounded-md p-3">
- <div className="flex items-start gap-2">
- <AlertCircle className="h-4 w-4 text-yellow-600 mt-0.5" />
- <div className="text-sm">
- <div className="font-medium text-yellow-800">도메인 변경 주의</div>
- <div className="text-yellow-700">
- 일부 부서의 기존 도메인과 다른 도메인을 할당하려고 합니다.
- 기존 할당은 자동으로 비활성화됩니다.
- </div>
- </div>
- </div>
- </div>
- )}
- </div>
- </>
- )}
-
- <Separator />
-
- {/* 도메인 선택 */}
- <div className="space-y-2">
- <Label htmlFor="domain-select" className="text-sm font-medium">
- 할당할 도메인 *
- </Label>
- <Select value={selectedDomain} onValueChange={setSelectedDomain}>
- <SelectTrigger id="domain-select">
- <SelectValue placeholder="도메인을 선택하세요" />
- </SelectTrigger>
- <SelectContent>
- {DOMAIN_OPTIONS.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- <div className="flex flex-col">
- <span className="font-medium">{option.label}</span>
- <span className="text-xs text-muted-foreground">
- {option.description}
- </span>
- </div>
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
-
- {selectedDomainInfo && (
- <div className="text-sm text-muted-foreground">
- <Badge variant="secondary" className="mr-2">
- {selectedDomainInfo.label}
- </Badge>
- {selectedDomainInfo.description}
- </div>
- )}
- </div>
-
- {/* 할당 사유/설명 */}
- <div className="space-y-2">
- <Label htmlFor="description" className="text-sm font-medium">
- 할당 사유 또는 설명 (선택사항)
- </Label>
- <Textarea
- id="description"
- placeholder="예: 구매 업무 담당자들에게 procurement 도메인 할당"
- value={description}
- onChange={(e) => setDescription(e.target.value)}
- rows={3}
- maxLength={500}
- />
- <div className="text-xs text-muted-foreground text-right">
- {description.length}/500
- </div>
- </div>
-
- {/* 주의사항 */}
- <div className="bg-muted/50 p-3 rounded-md">
- <div className="text-sm text-muted-foreground">
- <div className="font-medium mb-1">⚠️ 주의사항</div>
- <ul className="list-disc list-inside space-y-1 text-xs">
- <li>도메인 할당은 해당 부서 소속 사용자들의 메뉴 접근 권한에 영향을 줍니다.</li>
- <li>기존에 다른 도메인이 할당된 부서는 새로운 도메인으로 덮어씌워집니다.</li>
- <li>Knox 조직도 변경으로 인해 부서가 삭제된 경우, 해당 할당은 고립된 레코드가 됩니다.</li>
- </ul>
- </div>
- </div>
- </div>
- </div>
-
- <DialogFooter className="border-t pt-4">
- <Button
- variant="outline"
- onClick={() => onOpenChange(false)}
- disabled={isSubmitting || isLoading}
- >
- 취소
- </Button>
- <Button
- onClick={handleAssign}
- disabled={!canSubmit}
- >
- {isSubmitting || isLoading ? (
- <>
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
- 할당 중...
- </>
- ) : (
- `도메인 할당 (${selectedDepartments.length}개 부서)`
- )}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx
deleted file mode 100644
index c24770bf..00000000
--- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx
+++ /dev/null
@@ -1,309 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { useState, useTransition, useEffect } from "react";
-import { Settings, Plus, Users } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { toast } from "sonner";
-import { DepartmentTreeView } from "./department-tree-view";
-import { DepartmentDomainAssignmentDialog } from "./department-domain-assignment-dialog";
-import {
- type DepartmentNode
-} from "@/lib/users/knox-service";
-import {
- assignDomainToDepartments,
- getDepartmentDomainAssignments,
- autoAssignPendingUsersDomains,
- type UserDomain
-} from "@/lib/users/department-domain/service";
-import { DOMAIN_OPTIONS } from "./domain-constants";
-
-interface DepartmentMenuAccessManagerProps {
- departmentsPromise: Promise<DepartmentNode[]>;
- companyInfo: { code: string; name: string };
-}
-
-interface DepartmentAssignment {
- id: number;
- departmentCode: string;
- departmentName: string;
- assignedDomain: string;
- description?: string | null;
-}
-
-export function DepartmentMenuAccessManager({
- departmentsPromise,
- companyInfo
-}: DepartmentMenuAccessManagerProps) {
- const [departments, setDepartments] = useState<DepartmentNode[]>([]);
- const [selectedDepartments, setSelectedDepartments] = useState<string[]>([]);
- const [assignments, setAssignments] = useState<DepartmentAssignment[]>([]);
- const [isDialogOpen, setIsDialogOpen] = useState(false);
- const [isPending, startTransition] = useTransition();
- const [isDepartmentsLoading, setIsDepartmentsLoading] = useState(true);
- const [isAssignmentsLoading, setIsAssignmentsLoading] = useState(true);
-
- // Promise를 해결하여 부서 데이터 로드
- useEffect(() => {
- const loadDepartments = async () => {
- setIsDepartmentsLoading(true);
- try {
- const departmentTree = await departmentsPromise;
- setDepartments(departmentTree);
- } catch (error) {
- console.error("부서 트리 로드 실패:", error);
- toast.error("부서 정보를 불러오는데 실패했습니다.");
- setDepartments([]);
- } finally {
- setIsDepartmentsLoading(false);
- }
- };
-
- loadDepartments();
- }, [departmentsPromise]);
-
- // 기존 할당 정보 로드
- useEffect(() => {
- const loadAssignments = async () => {
- setIsAssignmentsLoading(true);
- try {
- const assignmentData = await getDepartmentDomainAssignments();
- setAssignments(assignmentData as DepartmentAssignment[]);
- } catch (error) {
- console.error("할당 정보 로드 실패:", error);
- toast.error("할당 정보를 불러오는데 실패했습니다.");
- setAssignments([]);
- } finally {
- setIsAssignmentsLoading(false);
- }
- };
-
- loadAssignments();
- }, []);
-
- // 선택된 부서들의 정보 가져오기
- const getSelectedDepartmentInfo = React.useCallback(() => {
- const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => {
- for (const node of nodes) {
- if (node.departmentCode === code) {
- return node;
- }
- const found = findDepartment(node.children, code);
- if (found) return found;
- }
- return null;
- };
-
- return selectedDepartments
- .map(code => findDepartment(departments, code))
- .filter(Boolean) as DepartmentNode[];
- }, [departments, selectedDepartments]);
-
- // 도메인 할당 처리
- const handleDomainAssign = async (assignmentData: {
- departmentCodes: string[];
- domain: string;
- description?: string;
- }) => {
- // 선택된 부서들의 이름 매핑 생성
- const departmentNames: Record<string, string> = {};
- const collectDepartmentNames = (nodes: DepartmentNode[]) => {
- nodes.forEach(node => {
- if (assignmentData.departmentCodes.includes(node.departmentCode)) {
- departmentNames[node.departmentCode] = node.departmentName || node.departmentCode;
- }
- collectDepartmentNames(node.children);
- });
- };
- collectDepartmentNames(departments);
-
- startTransition(async () => {
- try {
- const result = await assignDomainToDepartments({
- departmentCodes: assignmentData.departmentCodes,
- domain: assignmentData.domain as UserDomain,
- description: assignmentData.description,
- departmentNames,
- });
-
- if (result.success) {
- toast.success(result.message);
- setSelectedDepartments([]);
-
- // 할당 정보 새로고침
- try {
- const updatedAssignments = await getDepartmentDomainAssignments();
- setAssignments(updatedAssignments as DepartmentAssignment[]);
- } catch (error) {
- console.error("할당 정보 새로고침 실패:", error);
- }
-
- // users 테이블에 도메인 동기화 작업 진행
- try {
- const syncResult = await autoAssignPendingUsersDomains();
- if (syncResult.success && syncResult.assignedCount > 0) {
- toast.success(`사용자 도메인 동기화 완료: ${syncResult.assignedCount}명의 사용자가 자동 할당되었습니다.`);
- }
- } catch (error) {
- console.error("사용자 도메인 동기화 실패:", error);
- // 동기화 실패해도 메인 할당은 성공이므로 에러 토스트는 표시하지 않음
- }
- } else {
- toast.error(result.message);
- }
- } catch (error) {
- console.error("도메인 할당 실패:", error);
- toast.error("도메인 할당 중 오류가 발생했습니다.");
- }
- });
- };
-
- const canAssign = selectedDepartments.length > 0;
- const selectedDepartmentInfo = getSelectedDepartmentInfo();
-
- const isLoading = isDepartmentsLoading || isAssignmentsLoading;
-
- return (
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
- {/* 왼쪽: 조직도 트리 */}
- <div className="lg:col-span-2">
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Settings className="h-5 w-5" />
- 조직도 - {companyInfo.name}
- </CardTitle>
- <CardDescription>
- 부서를 선택하여 도메인을 할당하세요. 상위 부서 선택 시 하위 부서들도 자동으로 포함됩니다.
- </CardDescription>
- </CardHeader>
- <CardContent className="p-0">
- {isLoading ? (
- <div className="flex items-center justify-center h-[80vh]">
- <div className="text-center">
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
- <p className="text-muted-foreground">조직도를 불러오는 중...</p>
- </div>
- </div>
- ) : (
- <DepartmentTreeView
- departments={departments}
- selectedDepartments={selectedDepartments}
- onSelectionChange={setSelectedDepartments}
- assignments={assignments}
- />
- )}
- </CardContent>
- </Card>
- </div>
-
- {/* 오른쪽: 선택된 부서 정보 및 할당 버튼 */}
- <div className="space-y-6">
- {/* 선택된 부서 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Users className="h-5 w-5" />
- 선택된 부서
- </CardTitle>
- <CardDescription>
- {selectedDepartments.length}개 부서가 선택되었습니다
- </CardDescription>
- </CardHeader>
- <CardContent>
- {selectedDepartmentInfo.length === 0 ? (
- <div className="text-center py-4 text-muted-foreground">
- 부서를 선택해주세요
- </div>
- ) : (
- <div className="space-y-2 max-h-60 overflow-y-auto">
- {selectedDepartmentInfo.map((dept) => {
- const assignment = assignments.find(a => a.departmentCode === dept.departmentCode);
- return (
- <div
- key={dept.departmentCode}
- className="flex items-center justify-between p-2 bg-accent/20 rounded-md"
- >
- <div className="min-w-0">
- <div className="font-medium truncate">
- {dept.departmentName || dept.departmentCode}
- </div>
- <div className="text-xs text-muted-foreground">
- {dept.departmentCode}
- </div>
- </div>
- {assignment && (
- <Badge variant="outline" className="text-xs shrink-0">
- {assignment.assignedDomain}
- </Badge>
- )}
- </div>
- );
- })}
- </div>
- )}
- </CardContent>
- </Card>
-
- {/* 도메인 할당 버튼 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">도메인 할당</CardTitle>
- <CardDescription>
- 선택된 부서들에 도메인을 할당합니다
- </CardDescription>
- </CardHeader>
- <CardContent>
- <Button
- onClick={() => setIsDialogOpen(true)}
- disabled={!canAssign || isPending}
- size="lg"
- className="w-full"
- >
- <Plus className="mr-2 h-4 w-4" />
- 도메인 할당 ({selectedDepartments.length}개 부서)
- </Button>
-
- {canAssign && (
- <div className="mt-3 text-sm text-muted-foreground">
- 상위 부서를 선택한 경우 하위 부서들도 자동으로 동일한 도메인이 할당됩니다.
- </div>
- )}
- </CardContent>
- </Card>
-
- {/* 범례 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">도메인 범례</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 gap-2 text-sm">
- {DOMAIN_OPTIONS.map((option) => (
- <div key={option.value} className="flex items-center gap-2">
- <Badge className={option.color}>
- {option.value}
- </Badge>
- <span>{option.description}</span>
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
- </div>
-
- {/* 도메인 할당 다이얼로그 */}
- <DepartmentDomainAssignmentDialog
- open={isDialogOpen}
- onOpenChange={setIsDialogOpen}
- selectedDepartments={selectedDepartments}
- departments={departments}
- companyInfo={companyInfo}
- onAssign={handleDomainAssign}
- isLoading={isPending}
- />
- </div>
- );
-} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx
deleted file mode 100644
index 126f1eb7..00000000
--- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx
+++ /dev/null
@@ -1,343 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { ChevronDown, ChevronRight, Minus, Plus } from "lucide-react";
-import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Badge } from "@/components/ui/badge";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { DepartmentNode } from "@/lib/users/knox-service";
-import { getDomainLabel, getDomainColor } from "./domain-constants";
-
-interface DepartmentAssignment {
- id: number;
- departmentCode: string;
- assignedDomain: string;
- description?: string | null;
-}
-
-interface DepartmentTreeViewProps {
- departments: DepartmentNode[];
- selectedDepartments: string[];
- onSelectionChange: (selected: string[]) => void;
- assignments: DepartmentAssignment[];
- className?: string;
-}
-
-interface TreeNodeProps {
- node: DepartmentNode;
- selectedDepartments: string[];
- onToggle: (departmentCode: string) => void;
- expandedNodes: Set<string>;
- onExpandToggle: (departmentCode: string) => void;
- assignments: DepartmentAssignment[];
- level: number;
-}
-
-function TreeNode({
- node,
- selectedDepartments,
- onToggle,
- expandedNodes,
- onExpandToggle,
- assignments,
- level
-}: TreeNodeProps) {
- const isExpanded = expandedNodes.has(node.departmentCode);
- const hasChildren = node.children.length > 0;
-
- // 현재 부서에 할당된 도메인 찾기
- const assignment = assignments.find(a => a.departmentCode === node.departmentCode);
-
- // 현재 노드의 선택 상태 확인
- const isSelected = selectedDepartments.includes(node.departmentCode);
-
- // 하위 노드들 중 선택된 것이 있는지 확인 (부분 선택 상태 표시용)
- const hasSelectedChildren = React.useMemo(() => {
- if (!hasChildren) return false;
-
- const getAllChildCodes = (dept: DepartmentNode): string[] => {
- const codes: string[] = [];
- dept.children.forEach(child => {
- codes.push(child.departmentCode);
- codes.push(...getAllChildCodes(child));
- });
- return codes;
- };
-
- const childCodes = getAllChildCodes(node);
- return childCodes.some(code => selectedDepartments.includes(code));
- }, [node, selectedDepartments, hasChildren]);
-
- const handleToggle = () => {
- onToggle(node.departmentCode);
- };
-
- const handleExpandToggle = () => {
- if (hasChildren) {
- onExpandToggle(node.departmentCode);
- }
- };
-
- return (
- <div className="select-none">
- <div
- className={cn(
- "flex items-center gap-2 py-2 px-3 hover:bg-accent/50 rounded-md transition-colors",
- (isSelected || (!isSelected && hasSelectedChildren)) && "bg-accent/20"
- )}
- style={{ marginLeft: `${level * 16}px` }}
- >
- {/* 확장/축소 버튼 */}
- <div className="flex items-center justify-center w-5 h-5">
- {hasChildren ? (
- <Button
- variant="ghost"
- size="sm"
- className="h-5 w-5 p-0 hover:bg-transparent"
- onClick={handleExpandToggle}
- >
- {isExpanded ? (
- <ChevronDown className="h-3 w-3" />
- ) : (
- <ChevronRight className="h-3 w-3" />
- )}
- </Button>
- ) : null}
- </div>
-
- {/* 체크박스 */}
- <div className="flex items-center">
- <Checkbox
- checked={isSelected}
- onCheckedChange={handleToggle}
- className={cn(
- "h-4 w-4",
- !isSelected && hasSelectedChildren && "[&>*:first-child]:opacity-50"
- )}
- />
- </div>
-
- {/* 부서 정보 */}
- <div className="flex-1 min-w-0 cursor-pointer" onClick={handleToggle}>
- <div className="flex items-center gap-2">
- <span className={cn(
- "font-medium truncate",
- (isSelected || (!isSelected && hasSelectedChildren)) && "text-primary"
- )}>
- {node.departmentName || node.departmentCode}
- </span>
-
- {/* 할당된 도메인 표시 */}
- {assignment && (
- <Badge
- className={cn(
- "text-xs shrink-0",
- getDomainColor(assignment.assignedDomain)
- )}
- variant="outline"
- >
- {getDomainLabel(assignment.assignedDomain)}
- </Badge>
- )}
- </div>
-
- {/* 부서 코드 */}
- <div className="text-xs text-muted-foreground truncate">
- {node.departmentCode}
- {assignment?.description && (
- <span className="ml-1">• {assignment.description}</span>
- )}
- </div>
- </div>
- </div>
-
- {/* 하위 노드들 */}
- {hasChildren && isExpanded && (
- <div className="mt-1">
- {node.children.map((child) => (
- <TreeNode
- key={child.departmentCode}
- node={child}
- selectedDepartments={selectedDepartments}
- onToggle={onToggle}
- expandedNodes={expandedNodes}
- onExpandToggle={onExpandToggle}
- assignments={assignments}
- level={level + 1}
- />
- ))}
- </div>
- )}
- </div>
- );
-}
-
-export function DepartmentTreeView({
- departments,
- selectedDepartments,
- onSelectionChange,
- assignments,
- className,
-}: DepartmentTreeViewProps) {
- const [expandedNodes, setExpandedNodes] = React.useState<Set<string>>(new Set());
-
- // 부서 토글 핸들러
- const handleToggle = (departmentCode: string) => {
- const findNode = (nodes: DepartmentNode[], code: string): DepartmentNode | null => {
- for (const node of nodes) {
- if (node.departmentCode === code) return node;
- const found = findNode(node.children, code);
- if (found) return found;
- }
- return null;
- };
-
- const getAllChildCodes = (node: DepartmentNode): string[] => {
- const codes: string[] = [];
- node.children.forEach(child => {
- codes.push(child.departmentCode);
- codes.push(...getAllChildCodes(child));
- });
- return codes;
- };
-
- const targetNode = findNode(departments, departmentCode);
- if (!targetNode) return;
-
- const isCurrentlySelected = selectedDepartments.includes(departmentCode);
-
- let newSelected: string[];
- if (isCurrentlySelected) {
- // 선택 해제: 해당 부서만 제거 (하위 부서는 유지, 상위 부서에도 영향 없음)
- newSelected = selectedDepartments.filter(code => code !== departmentCode);
- } else {
- // 선택: 해당 부서 + 모든 하위 부서 추가
- const childCodes = getAllChildCodes(targetNode);
- const codesToAdd = [departmentCode, ...childCodes].filter(code => !selectedDepartments.includes(code));
- newSelected = [...selectedDepartments, ...codesToAdd];
- }
-
- onSelectionChange(newSelected);
- };
-
- // 노드 확장/축소 핸들러
- const handleExpandToggle = (departmentCode: string) => {
- const newExpanded = new Set(expandedNodes);
- if (newExpanded.has(departmentCode)) {
- newExpanded.delete(departmentCode);
- } else {
- newExpanded.add(departmentCode);
- }
- setExpandedNodes(newExpanded);
- };
-
- // 전체 확장/축소
- const handleExpandAll = () => {
- if (expandedNodes.size === 0) {
- const getAllCodes = (nodes: DepartmentNode[]): string[] => {
- const codes: string[] = [];
- nodes.forEach(node => {
- if (node.children.length > 0) {
- codes.push(node.departmentCode);
- codes.push(...getAllCodes(node.children));
- }
- });
- return codes;
- };
- setExpandedNodes(new Set(getAllCodes(departments)));
- } else {
- setExpandedNodes(new Set());
- }
- };
-
- // 전체 선택/해제
- const handleSelectAll = () => {
- if (selectedDepartments.length === 0) {
- // 전체 선택
- const allCodes: string[] = [];
- const collectCodes = (nodes: DepartmentNode[]) => {
- nodes.forEach(node => {
- allCodes.push(node.departmentCode);
- collectCodes(node.children);
- });
- };
- collectCodes(departments);
- onSelectionChange(allCodes);
- } else {
- // 전체 해제
- onSelectionChange([]);
- }
- };
-
- return (
- <div className={cn("border rounded-lg", className)}>
- {/* 헤더 */}
- <div className="flex items-center justify-between p-3 border-b bg-muted/30">
- <h3 className="font-medium">조직도</h3>
- <div className="flex gap-2">
- <Button
- variant="outline"
- size="sm"
- onClick={handleExpandAll}
- className="text-xs"
- >
- {expandedNodes.size === 0 ? (
- <>
- <Plus className="mr-1 h-3 w-3" />
- 전체 펼치기
- </>
- ) : (
- <>
- <Minus className="mr-1 h-3 w-3" />
- 전체 접기
- </>
- )}
- </Button>
- <Button
- variant="outline"
- size="sm"
- onClick={handleSelectAll}
- className="text-xs"
- >
- {selectedDepartments.length === 0 ? "전체 선택" : "선택 해제"}
- </Button>
- </div>
- </div>
-
- {/* 트리 본문 */}
- <ScrollArea className="h-[80vh] p-2">
- {departments.length === 0 ? (
- <div className="text-center py-8 text-muted-foreground">
- 부서 정보가 없습니다
- </div>
- ) : (
- <div className="space-y-1">
- {departments.map((dept) => (
- <TreeNode
- key={dept.departmentCode}
- node={dept}
- selectedDepartments={selectedDepartments}
- onToggle={handleToggle}
- expandedNodes={expandedNodes}
- onExpandToggle={handleExpandToggle}
- assignments={assignments}
- level={0}
- />
- ))}
- </div>
- )}
- </ScrollArea>
-
- {/* 푸터 */}
- {selectedDepartments.length > 0 && (
- <div className="border-t p-3 bg-muted/30">
- <div className="text-sm text-muted-foreground">
- 선택된 부서: <span className="font-medium text-foreground">{selectedDepartments.length}개</span>
- </div>
- </div>
- )}
- </div>
- );
-} \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/domain-constants.ts b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/domain-constants.ts
deleted file mode 100644
index 2b104d0e..00000000
--- a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/domain-constants.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-// 통합된 도메인 옵션 - 모든 도메인 정보를 포함
-export const DOMAIN_OPTIONS = [
- {
- value: "pending",
- label: "pending",
- description: "승인 대기 상태",
- color: "bg-yellow-100 text-yellow-800 border-yellow-200"
- },
- {
- value: "evcp",
- label: "evcp",
- description: "eVCP 시스템 관리자",
- color: "bg-blue-100 text-blue-800 border-blue-200"
- },
- {
- value: "procurement",
- label: "procurement",
- description: "구매",
- color: "bg-green-100 text-green-800 border-green-200"
- },
- {
- value: "sales",
- label: "sales",
- description: "기술영업",
- color: "bg-purple-100 text-purple-800 border-purple-200"
- },
- {
- value: "engineering",
- label: "engineering",
- description: "설계",
- color: "bg-orange-100 text-orange-800 border-orange-200"
- },
-] as const;
-
-// 헬퍼 함수들 - 필요시 매핑 객체 생성
-export const getDomainOption = (value: string) => {
- return DOMAIN_OPTIONS.find(option => option.value === value);
-};
-
-export const getDomainLabel = (value: string) => {
- return getDomainOption(value)?.label || value;
-};
-
-export const getDomainColor = (value: string) => {
- return getDomainOption(value)?.color || "bg-gray-100 text-gray-800 border-gray-200";
-};
-
-export const getDomainDescription = (value: string) => {
- return getDomainOption(value)?.description || value;
-}; \ No newline at end of file
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/page.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/page.tsx
deleted file mode 100644
index dfda9172..00000000
--- a/app/[lng]/evcp/(evcp)/menu-access-dept/page.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as React from "react";
-import { Separator } from "@/components/ui/separator";
-import { Shell } from "@/components/shell";
-import { DepartmentMenuAccessManager } from "./_components/department-menu-access-manager";
-import { getAllDepartmentsTree, getCurrentCompanyInfo } from "@/lib/users/knox-service";
-
-export default async function DepartmentMenuAccessPage() {
- // Promise들을 생성하여 클라이언트 컴포넌트에 전달
- const departmentsPromise = getAllDepartmentsTree();
- const companyInfo = await getCurrentCompanyInfo();
-
- return (
- <Shell>
- <div className="space-y-6">
- {/* 헤더 섹션 */}
- <div className="space-y-2">
- <h1 className="text-2xl font-bold tracking-tight">부서별 메뉴 접근권한 관리</h1>
- <p className="text-muted-foreground">
- Knox 조직도를 기반으로 부서별 도메인을 할당하여 메뉴 접근 권한을 관리할 수 있습니다.
- 상위 부서를 선택하면 하위 부서들도 자동으로 포함됩니다.
- </p>
- </div>
-
- <Separator />
-
- {/* 메인 관리 컴포넌트 */}
- <DepartmentMenuAccessManager
- departmentsPromise={departmentsPromise}
- companyInfo={companyInfo}
- />
- </div>
- </Shell>
- );
-}